<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="ROTEq" id="ROTEq"><span data-lake-id="uc80e65d4" id="uc80e65d4">典型回答</span></h1>
  <p data-lake-id="u4564d84a" id="u4564d84a"><span data-lake-id="ua14b1472" id="ua14b1472">​</span><br></p>
  <p data-lake-id="u654229d9" id="u654229d9"><span data-lake-id="u66094b44" id="u66094b44">虚拟线程这个名字很多人可能比较懵，但是如果对像Go、Ruby、python等语言有一些了解的话，就会很快的反应过来，其实这就是协程。</span></p>
  <p data-lake-id="ue58d3a68" id="ue58d3a68"><span data-lake-id="ufe44cd77" id="ufe44cd77">​</span><br></p>
  <p data-lake-id="uefc5bed0" id="uefc5bed0"><span data-lake-id="ua5c94dfb" id="ua5c94dfb">在以前的JDK中，Java的线程模型其实比较简单，在大多数操作系统中，主要采用的是基于轻量级进程实现的一对一的线程模型，简单来说就是每一个Java线程对应一个操作系统中的轻量级进程，这种线程模型中的线程创建、析构及同步等动作，都需要进行系统调用。而系统调用则需要在用户态（User Mode）和内核态（Kernel Mode）中来回切换，所以性能开销还是很大的。</span></p>
  <p data-lake-id="ud59ffc6e" id="ud59ffc6e"><span data-lake-id="u633df971" id="u633df971">​</span><br></p>
  <p data-lake-id="u38d800fa" id="u38d800fa"><span data-lake-id="u44341fd4" id="u44341fd4">而新引入的虚拟线程，是JDK 实现的轻量级线程，他可以避免上下文切换带来的的额外耗费。他的实现原理其实是JDK不再是每一个线程都一对一的对应一个操作系统的线程了，而是会将多个虚拟线程映射到少量操作系统线程中，通过有效的调度来避免那些上下文切换。</span></p>
  <p data-lake-id="uac838504" id="uac838504"><span data-lake-id="u92f9bd7c" id="u92f9bd7c">​</span><br></p>
  <p data-lake-id="u454f457d" id="u454f457d"><span data-lake-id="u5b2025b8" id="u5b2025b8">在JDK 21，有多种方法可以创建协程，如Thread.startVirtualThread()、Executors.newVirtualThreadPerTaskExecutor()等。</span></p>
  <h1 data-lake-id="m4rwK" id="m4rwK"><span data-lake-id="ue3226a22" id="ue3226a22">扩展知识</span></h1>
  <p data-lake-id="u91182d62" id="u91182d62"><br></p>
  <h2 data-lake-id="4683f754" id="4683f754"><span data-lake-id="u3eea1b52" id="u3eea1b52">线程的实现方式</span></h2>
  <p data-lake-id="u721acbc4" id="u721acbc4"><br></p>
  <p data-lake-id="ucafe7ad6" id="ucafe7ad6"><span data-lake-id="u44b7ae00" id="u44b7ae00">我们都知道，在操作系统中，线程是比进程更轻量级的调度执行单位，线程的引入可以把一个进程的资源分配和执行调度分开，各个线程既可以共享进程资源，又可以独立调度。</span></p>
  <p data-lake-id="u774ab654" id="u774ab654"><br></p>
  <p data-lake-id="u9efdfe5d" id="u9efdfe5d"><span data-lake-id="uc430106a" id="uc430106a">其实，</span><strong><span data-lake-id="u91fe6af9" id="u91fe6af9">线程的实现方式主要有三种：分别是使用内核线程实现、使用用户线程实现以及使用用户线程加轻量级进程混合实现。</span></strong></p>
  <p data-lake-id="ucb6e7986" id="ucb6e7986"><br></p>
  <h3 data-lake-id="8bcd0c28" id="8bcd0c28"><span data-lake-id="u026cb131" id="u026cb131">使用内核线程实现</span></h3>
  <p data-lake-id="ua9187d55" id="ua9187d55"><br></p>
  <p data-lake-id="u89099c66" id="u89099c66"><strong><span data-lake-id="uf6e82abb" id="uf6e82abb">内核线程（Kernel-Level Thread,KLT）就是直接由操作系统内核（Kernel）支持的线程，这种线程由内核来完成线程切换，内核通过操纵调度器（Scheduler）对线程进行调度，并负责将线程的任务映射到各个处理器上，并向应用程序提供API接口来管理线程。</span></strong></p>
  <p data-lake-id="u59a6e76f" id="u59a6e76f"><br></p>
  <p data-lake-id="ud5fd9191" id="ud5fd9191"><span data-lake-id="u2dc22da5" id="u2dc22da5">应用程序一般不会直接去使用内核线程，而是去使用内核线程的一种高级接口——轻量级进程（Light Weight Process,LWP），轻量级进程就是我们通常意义上所讲的线程，由于每个轻量级进程都由一个内核线程支持，因此只有先支持内核线程，才能有轻量级进程。</span></p>
  <p data-lake-id="u9ce2875f" id="u9ce2875f"><br></p>
  <p data-lake-id="u8e8f349b" id="u8e8f349b"><span data-lake-id="u440a5c59" id="u440a5c59">有了内核线程的支持，每个轻量级进程都成为一个独立的调度单元，即使有一个轻量级进程在系统调用中阻塞了，也不会影响整个进程继续工作。</span></p>
  <p data-lake-id="ud3764156" id="ud3764156"><br></p>
  <p data-lake-id="u37217707" id="u37217707"><span data-lake-id="u8ce27096" id="u8ce27096">但是轻量级进程具有它的局限性：首先，由于是基于内核线程实现的，所以各种线程操作，如创建、析构及同步，都需要进行系统调用。而系统调用的代价相对较高，需要在用户态（User Mode）和内核态（Kernel Mode）中来回切换。其次，每个轻量级进程都需要有一个内核线程的支持，因此轻量级进程要消耗一定的内核资源（如内核线程的栈空间），因此一个系统支持轻量级进程的数量是有限的。</span></p>
  <p data-lake-id="u04bdc1d1" id="u04bdc1d1"><br></p>
  <h3 data-lake-id="8e4981fa" id="8e4981fa"><span data-lake-id="ufe6a0e6d" id="ufe6a0e6d">使用用户线程实现</span></h3>
  <p data-lake-id="u4054cdde" id="u4054cdde"><br></p>
  <p data-lake-id="ud04b55af" id="ud04b55af"><span data-lake-id="ue20438b1" id="ue20438b1">在用户空间建立线程库，通过运行时系统(Run-time System)来完成线程的管理，因为这种线程的实现是在用户空间的，所以操作系统的内核并不知道线程的存在，所以内核管理的还是进程，所以这种线程的切换不需要内核操作。</span></p>
  <p data-lake-id="u7203730f" id="u7203730f"><br></p>
  <p data-lake-id="u4cf8eb86" id="u4cf8eb86"><span data-lake-id="ub8fc39f8" id="ub8fc39f8">这种实现方式下，进程和线程之间的关系是一对多的。</span></p>
  <p data-lake-id="u26cf0c47" id="u26cf0c47"><br></p>
  <p data-lake-id="u5d295d7e" id="u5d295d7e"><span data-lake-id="u52d43cc5" id="u52d43cc5">这种线程实现方式的优点是线程切换快，并且可以运行在任何操作系统之上，只需要实现线程库就行了。</span><strong><span data-lake-id="u14b126d8" id="u14b126d8">但是缺点也比较明显，就是所有线程的操作都需要用户程序自己处理，并且因为大多数系统调用都是阻塞的，所以一旦一个进程阻塞了，那么进程中的所有线程也会被阻塞。还有就是多处理器系统中如何将线程映射到其他处理器上也是一个比较大的问题。</span></strong></p>
  <p data-lake-id="u7b71d0d0" id="u7b71d0d0"><br></p>
  <h3 data-lake-id="af2d12f8" id="af2d12f8"><span data-lake-id="uc057acbc" id="uc057acbc">使用用户线程加轻量级进程混合实现</span></h3>
  <p data-lake-id="u198d30e8" id="u198d30e8"><br></p>
  <p data-lake-id="u7ce7fcf9" id="u7ce7fcf9"><span data-lake-id="u6c12ec5c" id="u6c12ec5c">还有一种混合实现的方式，就是线程的创建在用户空间完成，通过线程库进行，但是线程的调度是由内核来完成的。多个用户线程通过多路复用来复用多个内核线程。这个就不展开讲了。</span></p>
  <p data-lake-id="u888936e6" id="u888936e6"><br></p>
  <h2 data-lake-id="39024a28" id="39024a28"><span data-lake-id="uf7246b26" id="uf7246b26">Java的线程实现</span></h2>
  <p data-lake-id="u788e5f5d" id="u788e5f5d"><br></p>
  <p data-lake-id="u885d4fca" id="u885d4fca"><span data-lake-id="ud459ced1" id="ud459ced1">以上讲的是操作系统的线程的实现的三种方式，不同的操作系统在实现线程的时候会采用不同的机制，比如windows采用的是内核线程实现的，而Solaris则是通过混合模式实现的。</span></p>
  <p data-lake-id="u1fc665cf" id="u1fc665cf"><br></p>
  <p data-lake-id="uae5ae758" id="uae5ae758"><span data-lake-id="ub3587b6e" id="ub3587b6e">而Java作为一门跨平台的编程语言，实际上他的线程的实现其实是依赖具体的操作系统的。而比较常用的windows和linux来说，都是采用的内核线程的方式实现的。</span></p>
  <p data-lake-id="u014d8e77" id="u014d8e77"><br></p>
  <p data-lake-id="udd39127c" id="udd39127c"><span data-lake-id="u5ac1f615" id="u5ac1f615">也就是说，当我们在JAVA代码中创建一个Thread的时候，其实是需要映射到操作系统的线程的具体实现的，因为常见的通过内核线程实现的方式在创建、调度时都需要进行内核参与，所以成本比较高，尽管JAVA中提供了线程池的方式来避免重复创建线程，但是依旧有很大的优化空间。</span><strong><span data-lake-id="ufc442ec0" id="ufc442ec0">而且这种实现方式意味着受机器资源的影响，平台线程数也是有限制的。</span></strong></p>
  <p data-lake-id="uc1074177" id="uc1074177"><br></p>
  <h2 data-lake-id="7feaf14f" id="7feaf14f"><span data-lake-id="u8dd6b8fd" id="u8dd6b8fd">虚拟线程</span></h2>
  <p data-lake-id="u1b3ddd61" id="u1b3ddd61"><br></p>
  <p data-lake-id="u32d12205" id="u32d12205"><strong><span data-lake-id="u1772ba1f" id="u1772ba1f">JDK 19引入的虚拟线程，是JDK 实现的轻量级线程，他可以避免上下文切换带来的的额外耗费。</span></strong><span data-lake-id="uc87e0d39" id="uc87e0d39">他的实现原理其实是JDK不再是每一个线程都一对一的对应一个操作系统的线程了，而是会将多个虚拟线程映射到少量操作系统线程中，通过有效的调度来避免那些上下文切换。</span></p>
  <p data-lake-id="u92411a1b" id="u92411a1b"><br></p>
  <p data-lake-id="u11b6461c" id="u11b6461c"><img src="http://www.hollischuang.com/wp-content/uploads/2022/10/Java-Virtual-Threads-768x326-1.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_22%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uce3496ca" id="uce3496ca"><br></p>
  <p data-lake-id="ud07aad2d" id="ud07aad2d"><span data-lake-id="uea125e30" id="uea125e30">而且，我们可以在应用程序中创建非常多的虚拟线程，而不依赖于平台线程的数量。这些虚拟线程是由JVM管理的，因此它们不会增加额外的上下文切换开销，因为它们作为普通Java对象存储在RAM中。</span></p>
  <p data-lake-id="u48d0c8a9" id="u48d0c8a9"><br></p>
  <h2 data-lake-id="060f6378" id="060f6378"><span data-lake-id="u94f00fe6" id="u94f00fe6">虚拟线程和平台线程的区别</span></h2>
  <p data-lake-id="u3425fa1f" id="u3425fa1f"><br></p>
  <p data-lake-id="u311a9a49" id="u311a9a49"><span data-lake-id="ua9578d14" id="ua9578d14">首先，虚拟线程总是守护线程。setDaemon (false)方法不能将虚拟线程更改为非守护线程。</span><strong><span data-lake-id="u3fe4aebe" id="u3fe4aebe">所以，需要注意的是，当所有启动的非守护线程都终止时，JVM将终止。这意味着JVM不会等待虚拟线程完成后才退出。</span></strong></p>
  <p data-lake-id="ucff3d325" id="ucff3d325"><br></p>
  <p data-lake-id="u5e5bc4d4" id="u5e5bc4d4"><span data-lake-id="u0a4a4fc0" id="u0a4a4fc0">其次，即使使用setPriority()方法，</span><strong><span data-lake-id="uf26ce02f" id="uf26ce02f">虚拟线程始终具有normal的优先级</span></strong><span data-lake-id="u9bf69cbd" id="u9bf69cbd">，且不能更改优先级。在虚拟线程上调用此方法没有效果。</span></p>
  <p data-lake-id="u61a7460a" id="u61a7460a"><br></p>
  <p data-lake-id="u186c310f" id="u186c310f"><span data-lake-id="u5aafae72" id="u5aafae72">还有就是，</span><strong><span data-lake-id="u3b351e56" id="u3b351e56">虚拟线程是不支持stop()、suspend()或resume()等方法</span></strong><span data-lake-id="u151fc07c" id="u151fc07c">。这些方法在虚拟线程上调用时会抛出UnsupportedOperationException异常。</span></p>
  <p data-lake-id="u83867449" id="u83867449"><br></p>
  <h2 data-lake-id="481feccf" id="481feccf"><span data-lake-id="u3517184a" id="u3517184a">如何使用</span></h2>
  <p data-lake-id="ufd0431a4" id="ufd0431a4"><br></p>
  <p data-lake-id="u2205a8af" id="u2205a8af"><span data-lake-id="u93839aa7" id="u93839aa7">接下来介绍一下，在JDK 19中如何使用虚拟线程。</span></p>
  <p data-lake-id="ub9a55db3" id="ub9a55db3"><br></p>
  <p data-lake-id="ucc91b9bb" id="ucc91b9bb"><span data-lake-id="ue745b8c4" id="ue745b8c4">首先，通过Thread.startVirtualThread()可以运行一个虚拟线程：</span></p>
  <p data-lake-id="u2364b7d7" id="u2364b7d7"><br></p>
  <pre lang="java"><code>
Thread.startVirtualThread(() -&gt; {
    System.out.println("虚拟线程执行中...");
});
</code></pre>
  <p data-lake-id="ub25a4707" id="ub25a4707"><br></p>
  <p data-lake-id="u797cdf6d" id="u797cdf6d"><span data-lake-id="u37afcafe" id="u37afcafe">其次，通过Thread.Builder也可以创建虚拟线程，Thread类提供了ofPlatform()来创建一个平台线程、ofVirtual()来创建虚拟线程。</span></p>
  <p data-lake-id="uf2525907" id="uf2525907"><br></p>
  <pre lang="java"><code>
Thread.Builder platformBuilder = Thread.ofPlatform().name("平台线程");
Thread.Builder virtualBuilder = Thread.ofVirtual().name("虚拟线程");

Thread t1 = platformBuilder .start(() -&gt; {...}); 
Thread t2 = virtualBuilder.start(() -&gt; {...});
</code></pre>
  <p data-lake-id="u10e92855" id="u10e92855"><br></p>
  <p data-lake-id="u7052b47b" id="u7052b47b"><span data-lake-id="u0089b5b1" id="u0089b5b1">另外，线程池也支持了虚拟线程，可以通过Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程：</span></p>
  <p data-lake-id="u2c6661c1" id="u2c6661c1"><br></p>
  <pre lang="java"><code>
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10000).forEach(i -&gt; {
        executor.submit(() -&gt; {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}
</code></pre>
  <p data-lake-id="u3f17644c" id="u3f17644c"><br></p>
  <p data-lake-id="ueb5a2d1c" id="ueb5a2d1c"><span data-lake-id="uf5742b48" id="uf5742b48">但是，</span><strong><span data-lake-id="uf4164df0" id="uf4164df0">其实并不建议虚拟线程和线程池一起使用</span></strong><span data-lake-id="u5869ae50" id="u5869ae50">，因为Java线程池的设计是为了避免创建新的操作系统线程的开销，但是创建虚拟线程的开销并不大，所以其实没必要放到线程池中。</span></p>
  <p data-lake-id="u5e53e400" id="u5e53e400"><br></p>
  <h2 data-lake-id="1498c7ec" id="1498c7ec"><span data-lake-id="ud9c640de" id="ud9c640de">性能差异</span></h2>
  <p data-lake-id="u348ed96f" id="u348ed96f"><br></p>
  <p data-lake-id="ue1c67167" id="ue1c67167"><span data-lake-id="ucd954c4f" id="ucd954c4f">说了半天，虚拟线程到底能不能提升性能，能提升多少呢？我们来做个测试。</span></p>
  <p data-lake-id="ud6fe9f93" id="ud6fe9f93"><br></p>
  <p data-lake-id="uff515828" id="uff515828"><span data-lake-id="ue90c7e66" id="ue90c7e66">我们写一个简单的任务，在控制台中打印消息之前等待1秒：</span></p>
  <p data-lake-id="u9059a1c9" id="u9059a1c9"><br></p>
  <pre lang="java"><code>
final AtomicInteger atomicInteger = new AtomicInteger();

Runnable runnable = () -&gt; {
  try {
    Thread.sleep(Duration.ofSeconds(1));
  } catch(Exception e) {
      System.out.println(e);
  }
  System.out.println("Work Done - " + atomicInteger.incrementAndGet());
};
</code></pre>
  <p data-lake-id="ud9a84a42" id="ud9a84a42"><br></p>
  <p data-lake-id="ue2f23821" id="ue2f23821"><span data-lake-id="u25c86a28" id="u25c86a28">现在，我们将从这个Runnable创建10,000个线程，并使用虚拟线程和平台线程执行它们，以比较两者的性能。</span></p>
  <p data-lake-id="u8b245d2a" id="u8b245d2a"><br></p>
  <p data-lake-id="ua28dc627" id="ua28dc627"><span data-lake-id="uf0fa6e93" id="uf0fa6e93">先来我们比较熟悉的平台线程的实现：</span></p>
  <p data-lake-id="u5bdd5aa3" id="u5bdd5aa3"><br></p>
  <pre lang="java"><code>
Instant start = Instant.now();

try (var executor = Executors.newFixedThreadPool(100)) {
  for(int i = 0; i &lt; 10_000; i++) {
    executor.submit(runnable);
  }
}

Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("总耗时 : " + timeElapsed);
</code></pre>
  <p data-lake-id="ud6e6f28b" id="ud6e6f28b"><br></p>
  <p data-lake-id="u259dd055" id="u259dd055"><span data-lake-id="uae9afcbe" id="uae9afcbe">输出结果为：</span></p>
  <p data-lake-id="u6eafd1dd" id="u6eafd1dd"><br></p>
  <pre lang="java"><code>
总耗时 : 102323
</code></pre>
  <p data-lake-id="u4422dd4b" id="u4422dd4b"><br></p>
  <p data-lake-id="u0cc8a316" id="u0cc8a316"><span data-lake-id="u1d16ac0f" id="u1d16ac0f">总耗时大概100秒左右。接下来再用虚拟线程跑一下看看</span></p>
  <p data-lake-id="u6ec60702" id="u6ec60702"><br></p>
  <blockquote data-lake-id="ub3c98ef4" id="ub3c98ef4">
   <p data-lake-id="u6aebb431" id="u6aebb431"><span data-lake-id="u9f605180" id="u9f605180">在JDK 21中已经是正式功能了，但是在JDK 19中，虚拟线程是一个预览API，默认是禁用。所以需要使用$ java——source 19——enable-preview xx.java 的方式来运行代码。</span></p>
  </blockquote>
  <p data-lake-id="u3c1e2b25" id="u3c1e2b25"><span data-lake-id="u8991fab8" id="u8991fab8">​</span><br></p>
  <p data-lake-id="u32336ca4" id="u32336ca4"><br></p>
  <pre lang="java"><code>
Instant start = Instant.now();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
  for(int i = 0; i &lt; 10_000; i++) {
    executor.submit(runnable);
  }
}

Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("总耗时 : " + timeElapsed);
</code></pre>
  <p data-lake-id="u675c3dae" id="u675c3dae"><br></p>
  <p data-lake-id="u91f68fdd" id="u91f68fdd"><span data-lake-id="u024c4dcf" id="u024c4dcf">使用 Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程，执行结果如下：</span></p>
  <p data-lake-id="ue5ad501b" id="ue5ad501b"><br></p>
  <pre lang="java"><code>
总耗时 : 1674
</code></pre>
  <p data-lake-id="u544e3353" id="u544e3353"><br></p>
  <p data-lake-id="ub1d52bc7" id="ub1d52bc7"><span data-lake-id="u74f6f1f5" id="u74f6f1f5">总耗时大概1.6秒左右。</span></p>
  <p data-lake-id="u26614139" id="u26614139"><br></p>
  <p data-lake-id="ua7d1f460" id="ua7d1f460"><span data-lake-id="uf64d42de" id="uf64d42de">100秒和1.6秒的差距，足以看出虚拟线程的性能提升还是立竿见影的。</span></p>
  <p data-lake-id="u0267fcb3" id="u0267fcb3"><br></p>
 </body>
</html>